Skip to content

Release the outbound recycle guard on local-input EOF - #31

Merged
myobie merged 2 commits into
mainfrom
fix/outbound-gauge-local-input-eof
Aug 3, 2026
Merged

Release the outbound recycle guard on local-input EOF#31
myobie merged 2 commits into
mainfrom
fix/outbound-gauge-local-input-eof

Conversation

@myobie

@myobie myobie commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes the Linux defect located by draft PR #30's diagnostics: the outbound recycle guard was pinned for as long as the attach lived, and on Linux that outlasted the local side by more than 40s with the server still reporting the session attached, so endpoint repair became impossible.

The guard is now released on local-input EOF. Only the guard — the session, its buffering and its replay are untouched, because a half-closed client can still be reading output.

Proofs (each verified against a control that fails without the fix): live bidirectional client still blocks; local-input EOF releases within a bound that is load-bearing because the queued remote work takes longer than it; a half-closed reader receives delayed output exactly once across the now-allowed recycle; a reconnecting session does not block.

The underlying Linux teardown latency is deliberately out of scope and filed separately.

myobie added 2 commits August 3, 2026 12:14
Fixes the Linux defect the diagnostics branch located, and corrects the reasoning
I got wrong the first time.

The guard was released when the whole attach returned. On Linux that attach can
stay up long after the local side is finished: the forced trace measured
released=false at 40001ms with the SERVER still reporting srv_active_attaches=1
and srv_detached=0, where macOS detaches the same sequence in under 300ms. So the
guard pinned the endpoint for the whole stall and endpoint repair became
impossible on exactly the hosts we were about to deploy to. That is worse than the
disruption the guard was added to prevent.

The guard is now released when the local INPUT side reaches EOF. What I must not
claim, and did claim in my first proposal, is that no user remains at that point:
a client can half-close its input and go on reading output. So only the recycle
guard is released. The session, its buffering and its replay are untouched, and a
half-closed reader keeps receiving remote output across the recycle that releasing
allowed.

The guard now lives on the session rather than in the attach's stack frame, so
whichever comes first releases it: local-input EOF, or the attach ending. Nothing
is held between attaches, which is what keeps a reconnecting session from blocking
the very recycle that might let it reconnect.

Proofs, each checked against a control:
- A live bidirectional local client still blocks the recycle.
- Local-input EOF releases the guard within 500ms. The bound is load-bearing on
  purpose: the queued remote command sleeps a second before printing, so a release
  inside that window can only come from local-input EOF. With a generous timeout
  instead, the test passed even with the fix reverted, because macOS tears down in
  ~300ms and the two paths were indistinguishable. That is the third time in this
  work a no-change assertion needed a control to be worth anything.
- A half-closed reader receives the delayed output exactly once across the
  now-allowed recycle: counted, not merely sighted, so a duplicate would fail.
- A reconnecting session does not block the recycle; proven by stopping the peer
  so the session is genuinely between attaches rather than racily so.

The underlying Linux teardown latency is NOT addressed here and is being filed
separately with the trace.

Gates, macOS: shell 12, lib 173, provisioning 10, sync_slice 3, local_slice 21.
… EOF

PR 31's first commit fixed the clean-EOF case and left the Linux CI failure
exactly as it was, because the case that fails there is the other one.

run_local_reader propagated a read error with ? and returned before reaching
mark_send_closed, so an abrupt local close never released the recycle guard.
Dropping a local socket yields a clean zero-length read on macOS and an error on
Linux, which is why this was invisible locally and why my first fix looked
complete while changing nothing about the failing test.

Only the guard is released on that path. send_closed and every other teardown
semantic stay as they were: an error path is not where I want to be changing
session lifecycle, and the underlying Linux teardown latency is filed separately.
@myobie
myobie merged commit 1edbf6d into main Aug 3, 2026
2 checks passed
@myobie
myobie deleted the fix/outbound-gauge-local-input-eof branch August 4, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant